﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace Cassandraemon.Connection
{
	public class ManualNodeEnumerator
		: ICassandraNodeEnumerator
	{
		#region Constructors

		public ManualNodeEnumerator(ConnectionPool pool)
			: this(pool.Nodes)
		{
			this._Pool = pool;
		}

		public ManualNodeEnumerator(IEnumerable<string> hosts)
		{
			this.SetNodes(hosts);
		}

		#endregion

		#region Pool

		readonly ConnectionPool _Pool;

		protected ConnectionPool Pool
		{
			get { return _Pool; }
		}

		#endregion

		#region Nodes

		string[] _OriginalNodes;
		LinkedList<string> _Nodes;
		HashSet<string> _ExcludeNodes;

		public ICollection<string> Nodes
		{
			get { return _Nodes; }
		}

		protected internal void SetNodes(IEnumerable<string> hosts)
		{
			this.SetNodesCore(hosts.ToArray(), this._ExcludeNodes);
		}

		public void SetExcludes(IEnumerable<string> excludes)
		{
			var hosts = new HashSet<string>();
			foreach (var host in excludes)
			{
				hosts.Add(host);
				try
				{
					foreach (var ip in Dns.GetHostAddresses(host))
					{
						hosts.Add(ip.ToString());
					}
				}
				catch (SocketException)
				{
				}
			}

			if (hosts.Count == 0)
			{
				if (this._ExcludeNodes == null)
				{
					return; // not changed
				}
				this.SetNodesCore(this._OriginalNodes, null);
				return;
			}

			if (this._ExcludeNodes != null && hosts.SetEquals(this._ExcludeNodes))
			{
				return; // not changed
			}

			this.SetNodesCore(this._OriginalNodes, hosts);
		}

		void SetNodesCore(string[] hosts, HashSet<string> excludes)
		{
			if (hosts == null || hosts.Length == 0)
			{
				throw new ArgumentNullException("hosts");
			}

			LinkedList<string> list;
			if (excludes == null || excludes.Count == 0)
			{
				list = new LinkedList<string>(hosts);
			}
			else
			{
				list = new LinkedList<string>(hosts
					.Where(h => !excludes.Contains(h)));

				// Check if we have any nodes. If empty, ignore "excludes".

				if (list.Count == 0)
				{
					CassandraContext.TraceWarning("Excludes ignored to avoid empty nodes: nodes={0}, excludes={1}",
						string.Join(", ", hosts),
						string.Join(", ", excludes.ToArray()));
					list = new LinkedList<string>(hosts);
				}
			}

			this._OriginalNodes = hosts;
			this._ExcludeNodes = excludes;
			this._Nodes = list;

			this.OnNodesChanged(EventArgs.Empty);
		}

		protected virtual void OnNodesChanged(EventArgs e)
		{
			var pool = this._Pool;
			if (pool != null)
			{
				var nodes = this.Nodes;
				pool.UpdateNodes(nodes);
			}
		}

		protected void RoundRobin()
		{
			var nodes = this._Nodes;
			switch (nodes.Count)
			{
				case 0:
					throw new InvalidOperationException("No hosts in the ring");
				case 1:
					break;
				default:
					var first = nodes.First;
					nodes.RemoveFirst();
					nodes.AddLast(first);
					break;
			}
		}

		#endregion

		#region ICassandraNodeEnumerator

		public IEnumerable<string> NextNodes()
		{
			this.RoundRobin();
			return this._Nodes;
		}

		public virtual void UpdateNodes()
		{
		}

		#endregion
	}
}
